﻿using Mapster;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;

namespace Devonline.AspNetCore;

/// <summary>
/// 业务数据及其附件操作类服务
/// </summary>
/// <typeparam name="TDbContext">数据库上下文</typeparam>
/// <typeparam name="TEntitySet">业务数据类型</typeparam>
/// <typeparam name="TAttachment">附件数据类型</typeparam>
/// <typeparam name="TKey">主键类型</typeparam>
public class DataWithAttachmentService<TDbContext, TEntitySet, TAttachment, TKey> :
    DataService<TDbContext, TEntitySet, TKey>,
    IDataWithAttachmentService<TDbContext, TEntitySet, TAttachment, TKey>,
    IAttachmentService<TDbContext, TAttachment, TKey>,
    IDataService<TDbContext, TEntitySet, TKey>
    where TDbContext : DbContext
    where TEntitySet : class, IEntitySet<TKey>
    where TAttachment : class, IAttachment<TKey>
    where TKey : IConvertible
{
    protected readonly DbSet<TAttachment> _dbSetAttachment;
    protected readonly IAttachmentService<TDbContext, TAttachment, TKey> _attachmentService;
    public DataWithAttachmentService(
        TDbContext context,
        ILogger<DataWithAttachmentService<TDbContext, TEntitySet, TAttachment, TKey>> logger,
        IDistributedCache cache,
        IHttpContextAccessor httpContextAccessor,
        AppSetting appSetting,
        IAttachmentService<TDbContext, TAttachment, TKey> attachmentService
        ) : base(context, logger, cache, httpContextAccessor, appSetting)
    {
        _dbSetAttachment = _context.Set<TAttachment>();
        _attachmentService = attachmentService;
    }

    #region 实现自 IAttachmentService
    /// <summary>
    /// 获取并返回文件名指向的物理文件
    /// </summary>
    /// <param name="fileName">文件名</param>
    /// <returns></returns>
    public IActionResult GetFile(string fileName) => _attachmentService.GetFile(fileName);
    /// <summary>
    /// 文件下载, 提供类似于文件夹目录结构的访问方式进行下载
    /// 形如 2020-01-01/xxx.jpg
    /// </summary>
    /// <param name="fileName">文件名</param>
    /// <param name="filePath">文件路径</param>
    /// <returns></returns>
    public Task<TAttachment?> GetAttachmentAsync(string? fileName, string? filePath = null) => _attachmentService.GetAttachmentAsync(fileName, filePath);
    /// <summary>
    /// 获取附件对应的文件, 适用于文件下载
    /// 形如 2020-01-01/xxx.jpg
    /// </summary>
    /// <param name="fileName">文件名</param>
    /// <param name="filePath">文件路径</param>
    /// <returns></returns>
    public Task<IActionResult> GetAttachmentFileAsync(string? fileName, string? filePath = null) => _attachmentService.GetAttachmentFileAsync(fileName, filePath);
    /// <summary>
    /// 获取文件, 按业务类型和业务主键获取
    /// </summary>
    /// <param name="businessType">业务类型</param>
    /// <param name="businessKey">业务主键</param>
    /// <returns></returns>
    public Task<IEnumerable<TAttachment>?> GetAttachmentsAsync(string? businessType = null, TKey? businessKey = default) => _attachmentService.GetAttachmentsAsync(businessType, businessKey);

    /// <summary>
    /// 单个文件上传
    /// </summary>
    /// <param name="file"></param>
    /// <returns></returns>
    public Task<UploadResult<TAttachment, TKey>> UploadAsync(IFormFile? file = null) => _attachmentService.UploadAsync(file);
    /// <summary>
    /// 批量文件上传
    /// </summary>
    /// <param name="files"></param>
    /// <returns></returns>
    public Task<IEnumerable<UploadResult<TAttachment, TKey>>> UploadAsync(IEnumerable<IFormFile>? files = null) => _attachmentService.UploadAsync(files);

    /// <summary>
    /// 删除附件物理文件, 请在附件相关操作的事物提交之后执行此方法
    /// </summary>
    /// <param name="files">待删除的文件</param>
    public void Delete(IEnumerable<string> files) => _attachmentService.Delete(files);
    #endregion

    #region 实现自 IDataWithAttachmentService
    /// <summary>
    /// 新增附件
    /// </summary>
    /// <param name="entitySet">业务数据</param>
    /// <param name="attachments">附件集合</param>
    /// <param name="businessType">业务类型</param>
    /// <returns>待删除文件列表, 请在数据库事物提交成功后删除列表中的文件</returns>
    public async Task AddAttachmentsAsync(TEntitySet entitySet, IEnumerable<TAttachment>? attachments = null, string? businessType = null)
    {
        attachments ??= GetCollectionMembers<TAttachment>(entitySet);
        if (attachments != null && attachments.Any())
        {
            businessType ??= GetBusinessType();
            foreach (var attachment in attachments)
            {
                attachment.BusinessKey ??= entitySet.Id;
                attachment.BusinessType ??= businessType;
                Create(attachment);
                Detach(attachment);
                await _dbSetAttachment.AddAsync(attachment);
            }

            SetBusinessAttachmentPath(entitySet, attachments);
        }
    }
    /// <summary>
    /// 修改附件, 保留旧附件, 新增新附件, 返回待删除文件列表
    /// </summary>
    /// <param name="entitySet">业务数据</param>
    /// <param name="attachments">附件集合</param>
    /// <param name="businessType">业务类型</param>
    /// <param name="isLogical">是否逻辑操作, 默认不是</param>
    /// <returns>待删除文件列表, 请在数据库事物提交成功后删除列表中的文件</returns>
    public async Task<IEnumerable<string>> UpdateAttachmentsAsync(TEntitySet entitySet, IEnumerable<TAttachment>? attachments = null, string? businessType = null, bool isLogical = false)
    {
        businessType ??= GetBusinessType();
        attachments ??= GetCollectionMembers<TAttachment>(entitySet);
        var news = attachments ?? new List<TAttachment>();
        var olds = await _dbSetAttachment.Where(x => x.BusinessKey != null && x.BusinessKey.Equals(entitySet.Id)).ToListAsync() ?? new List<TAttachment>();
        foreach (var attachment in news)
        {
            if (!olds.Any(x => x.Path == attachment.Path || x.Id.Equals(attachment.Id)))
            {
                //新的有旧的没有, 则新增, 附件只有新增和删除, 没有修改的说法
                attachment.BusinessKey ??= entitySet.Id;
                attachment.BusinessType ??= businessType;
                Create(attachment);
                Detach(attachment);
                _dbSetAttachment.Add(attachment);
            }
        }

        var files = new List<string>();
        foreach (var attachment in olds)
        {
            //旧的有新的没有则删除
            //找 Id 或者 Path 值相等的, 新的里面找不到则删除旧的
            if ((!string.IsNullOrWhiteSpace(attachment.Path)) && (!news.Any(x => x.Id.Equals(attachment.Id) || x.Path == attachment.Path)))
            {
                files.Add(attachment.Path);
                Delete(attachment, isLogical);
            }
        }

        SetBusinessAttachmentPath(entitySet, attachments);
        return files;
    }
    /// <summary>
    /// 自动删除附件, 返回待删除文件列表, 适用于实体对象模型从数据库删除时查找原始附件并自动删除
    /// </summary>
    /// <param name="id">业务数据主键</param>
    /// <param name="businessType">业务类型</param>
    /// <param name="isLogical">是否逻辑操作, 默认不是</param>
    /// <returns>待删除文件列表, 请在数据库事物提交成功后删除列表中的文件</returns>
    public async Task<IEnumerable<string>?> DeleteAttachmentsAsync(TKey id, string? businessType = null, bool isLogical = false)
    {
        var queryable = _dbSetAttachment.Where(x => x.BusinessKey != null && x.BusinessKey.Equals(id));
        //if (!string.IsNullOrWhiteSpace(businessType))
        //{
        //    queryable = queryable.Where(x => x.BusinessType == businessType);
        //}

        var attachments = await queryable.ToListAsync();
        if (attachments.Count < 1)
        {
            return null;
        }

        var files = attachments.Select(x => x.Path ?? string.Empty).Distinct();
        foreach (var attachment in attachments)
        {
            Delete(attachment, isLogical);
        }

        return files;
    }
    /// <summary>
    /// 按文件列表删除附件
    /// </summary>
    /// <param name="fileNames">待删除的文件名</param>
    /// <param name="isLogical">是否逻辑操作, 默认不是</param>
    /// <returns></returns>
    public async Task<IEnumerable<string>?> DeleteAttachmentsAsync(IEnumerable<string> fileNames, bool isLogical = false)
    {
        var attachments = await _dbSetAttachment.Where(x => x.Path != null && fileNames.Contains(x.Path)).ToListAsync();
        if (attachments.Count < 1)
        {
            return null;
        }

        var files = attachments.Select(x => x.Path ?? string.Empty).Distinct();
        foreach (var attachment in attachments)
        {
            Delete(attachment, isLogical);
        }

        return files;
    }

    /// <summary>
    /// 新增附件
    /// </summary>
    /// <typeparam name="TModel">业务数据类型</typeparam>
    /// <param name="model">业务数据</param>
    /// <param name="attachments">附件集合</param>
    /// <param name="businessType">业务类型</param>
    /// <returns>待删除文件列表, 请在数据库事物提交成功后删除列表中的文件</returns>
    public async Task AddAttachmentsAsync<TModel>(TModel model, IEnumerable<TAttachment>? attachments = null, string? businessType = null) where TModel : class, IEntitySet<TKey>
    {
        attachments ??= GetCollectionMembers<TModel, TAttachment>(model);
        if (attachments != null && attachments.Any())
        {
            businessType ??= GetBusinessType();
            foreach (var attachment in attachments)
            {
                attachment.BusinessKey ??= model.Id;
                attachment.BusinessType ??= businessType;
                Create(attachment);
                Detach(attachment);
                await _dbSetAttachment.AddAsync(attachment);
            }

            SetBusinessAttachmentPath(model, attachments);
        }
    }
    /// <summary>
    /// 修改附件, 保留旧附件, 新增新附件, 返回待删除文件列表
    /// </summary>
    /// <typeparam name="TModel">业务数据类型</typeparam>
    /// <param name="model">业务数据</param>
    /// <param name="attachments">附件集合</param>
    /// <param name="businessType">业务类型</param>
    /// <param name="isLogical">是否逻辑操作, 默认不是</param>
    /// <returns>待删除文件列表, 请在数据库事物提交成功后删除列表中的文件</returns>
    public async Task<IEnumerable<string>> UpdateAttachmentsAsync<TModel>(TModel model, IEnumerable<TAttachment>? attachments = null, string? businessType = null, bool isLogical = false) where TModel : class, IEntitySet<TKey>
    {
        businessType ??= GetBusinessType();
        attachments ??= GetCollectionMembers<TModel, TAttachment>(model);
        var news = attachments ?? new List<TAttachment>();
        var olds = await _dbSetAttachment.Where(x => x.BusinessKey != null && x.BusinessKey.Equals(model.Id)).ToListAsync() ?? new List<TAttachment>();
        foreach (var attachment in news)
        {
            if (!olds.Any(x => x.Path == attachment.Path || x.Id.Equals(attachment.Id)))
            {
                //新的有旧的没有, 则新增, 附件只有新增和删除, 没有修改的说法
                attachment.BusinessKey ??= model.Id;
                attachment.BusinessType ??= businessType;
                Create(attachment);
                Detach(attachment);
                _dbSetAttachment.Add(attachment);
            }
        }

        var files = new List<string>();
        foreach (var attachment in olds)
        {
            //旧的有新的没有则删除
            //找 Id 或者 Path 值相等的, 新的里面找不到则删除旧的
            if ((!string.IsNullOrWhiteSpace(attachment.Path)) && (!news.Any(x => x.Id.Equals(attachment.Id) || x.Path == attachment.Path)))
            {
                files.Add(attachment.Path);
                Delete(attachment, isLogical);
            }
        }

        SetBusinessAttachmentPath(model, attachments);
        return files;
    }
    #endregion

    #region 重载自 DataService 的方法
    /// <summary>
    /// 此重载新增方法会同时将当前业务数据的附件一并新增
    /// </summary>
    /// <param name="entitySet">业务数据</param>
    /// <param name="context">数据操作上下文</param>
    /// <returns></returns>
    public override async Task<TEntitySet> AddAsync(TEntitySet entitySet, DataServiceContext? context = default)
    {
        _logger.LogInformation("The user {user} will add {typeName} and it's attachments, the content is: " + entitySet.ToJsonString(), UserName, _typeName);

        await ThrowIfExistAsync(entitySet.Id);
        await UniqueAsync(entitySet);
        Create(entitySet);
        Update(entitySet);
        Detach(entitySet);
        await _dbSet.AddAsync(entitySet);
        await AddAttachmentsAsync(entitySet, default, context?.BusinessType ?? default);
        await SaveChangesAsync(context);
        return entitySet;
    }
    /// <summary>
    /// 重载的更新方法会同时将当前业务数据的附件一并更新
    /// </summary>
    /// <param name="entitySet">业务数据</param>
    /// <param name="context">数据操作上下文</param>
    /// <returns></returns>
    public override async Task<TEntitySet> UpdateAsync(TEntitySet entitySet, DataServiceContext? context = default)
    {
        var isLogical = context?.IsLogical ?? false;
        _logger.LogDebug("The user {user} will {isLogical} update {typeName} and it's attachments, the content is: " + entitySet.ToJsonString(), UserName, GetLogicalString(isLogical), _typeName);

        await UniqueAsync(entitySet);
        var entity = await GetIfExistAsync(entitySet.Id);

        //先对原数据进行逻辑更新操作
        Update(entity, isLogical);

        //修改时先查了一次当前对象, 因此不能取消附加
        //Detach(entity);
        entitySet.Adapt(entity, context?.AdapterConfig ?? GetAdapterConfig());
        Update(entity);
        _dbSet.Update(entity);

        var files = await UpdateAttachmentsAsync(entitySet, default, context?.BusinessType ?? default, isLogical);
        if (files.Any())
        {
            context ??= new DataServiceContext();
            context.After += () => Delete(files);
        }

        await SaveChangesAsync(context);
        return entitySet;
    }
    /// <summary>
    /// 重载的删除方法会同时将当前业务数据的附件一并删除
    /// </summary>
    /// <param name="id">业务主键</param>
    /// <param name="context">数据操作上下文</param>
    /// <returns></returns>
    public override async Task DeleteAsync(TKey id, DataServiceContext? context = default)
    {
        var isLogical = context?.IsLogical ?? false;
        _logger.LogDebug("The user {user} will {isLogical} delete {typeName} and it's attachments, the content id is: " + id, UserName, GetLogicalString(isLogical), _typeName);
        var files = await DeleteAttachmentsAsync(id, context?.BusinessType ?? default, isLogical);
        if (files != null && files.Any())
        {
            context ??= new DataServiceContext();
            context.After += () => Delete(files);
        }

        await base.DeleteAsync(id, context);
    }

    /// <summary>
    /// 此重载新增方法会同时将当前业务数据的附件一并新增
    /// </summary>
    /// <typeparam name="TModel">业务数据类型或其视图类型</typeparam>
    /// <param name="model">业务数据或其视图</param>
    /// <param name="context">数据操作上下文</param>
    /// <returns></returns>
    public override async Task<TModel> AddAsync<TModel>(TModel model, DataServiceContext? context = default)
    {
        _logger.LogInformation("The user {user} will add {typeName} and it's attachments, the content is: " + model.ToJsonString(), UserName, typeof(TModel).Name);
        await AddAttachmentsAsync(model, default);
        return await base.AddAsync(model, context);
    }
    /// <summary>
    /// 重载的更新方法会同时将当前业务数据的附件一并更新
    /// </summary>
    /// <typeparam name="TModel">业务数据类型或其视图类型</typeparam>
    /// <param name="model">业务数据或其视图</param>
    /// <param name="context">数据操作上下文</param>
    /// <returns></returns>
    public override async Task<TModel> UpdateAsync<TModel>(TModel model, DataServiceContext? context = default)
    {
        var isLogical = context?.IsLogical ?? false;
        _logger.LogDebug("The user {user} will {isLogical} update {typeName} and it's attachments, the content is: " + model.ToJsonString(), UserName, GetLogicalString(isLogical), typeof(TModel).Name);
        await UpdateAttachmentsAsync(model, default, context?.BusinessType ?? default, isLogical);
        return await base.UpdateAsync(model, context);
    }
    #endregion

    /// <summary>
    /// 为当前业务对象设置附件路径
    /// </summary>
    /// <param name="entitySet">业务数据</param>
    /// <param name="attachments">业务数据的附件</param>
    protected virtual void SetBusinessAttachmentPath(TEntitySet entitySet, IEnumerable<TAttachment>? attachments)
    {
        var businessTypes = GetBusinessTypes();
        if (businessTypes.Any())
        {
            foreach (var businessType in businessTypes)
            {
                entitySet.SetPropertyValue(businessType.Key, attachments?.FirstOrDefault(x => x.BusinessType == businessType.Value)?.Path);
            }
        }
    }
    /// <summary>
    /// 为当前业务对象设置附件路径
    /// </summary>
    /// <typeparam name="TModel">业务数据类型或其视图类型</typeparam>
    /// <param name="model">业务数据或其视图</param>
    /// <param name="attachments">业务数据的附件</param>
    protected virtual void SetBusinessAttachmentPath<TModel>(TModel model, IEnumerable<TAttachment>? attachments) where TModel : class, IEntitySet<TKey>
    {
        //TODO 试图模型在获取 BusinessType 值时, 仅使用试图模型对应的业务对象类型
        var businessTypes = GetBusinessTypes();
        if (businessTypes.Any())
        {
            foreach (var businessType in businessTypes)
            {
                model.SetPropertyValue(businessType.Key, attachments?.FirstOrDefault(x => x.BusinessType == businessType.Value)?.Path);
            }
        }
    }
}

/// <summary>
/// 业务数据及其附件操作类服务
/// 字符串作为主键的默认实现
/// </summary>
/// <typeparam name="TDbContext">数据库上下文</typeparam>
/// <typeparam name="TEntitySet">业务数据类型</typeparam>
public class DataWithAttachmentService<TDbContext, TEntitySet> :
    DataWithAttachmentService<TDbContext, TEntitySet, Attachment, string>,
    IDataWithAttachmentService<TDbContext, TEntitySet>,
    IAttachmentService<TDbContext>,
    IDataService<TDbContext, TEntitySet>
    where TDbContext : DbContext
    where TEntitySet : class, IEntitySet<string>
{
    public DataWithAttachmentService(
        TDbContext context,
        ILogger<DataWithAttachmentService<TDbContext, TEntitySet>> logger,
        IDistributedCache cache,
        IHttpContextAccessor httpContextAccessor,
        AppSetting appSetting,
        IAttachmentService<TDbContext> attachmentService
        ) : base(context, logger, cache, httpContextAccessor, appSetting, attachmentService) { }
}